盒子
盒子
文章目录
  1. QEMU-QTest && Libfuzzer源码分析(下)
    1. 0x01 TL;DR
    2. 0x02 generic-fuzz
    3. 0x03 Summary
    4. 0x04 Reference

QEMU源码分析 - QTest(下)

QEMU-QTest && Libfuzzer源码分析(下)

0x01 TL;DR

续接上文,开始分析libfuzzer部分的代码。下文的前置知识基本在上文中已经覆盖。

0x02 generic-fuzz

文件在tests/qtest/fuzz/generic_fuzz.c中,入口函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void register_generic_fuzz_targets(void)
{
fuzz_add_target(&(FuzzTarget){
//......
});

GString *name;
const generic_fuzz_config *config;
//循环添加了好几种已经设置的device,文件在generic_fuzz_configs.h中,包含启动所需的一些command参数
for (int i = 0;
i < sizeof(predefined_configs) / sizeof(generic_fuzz_config);
i++) {
config = predefined_configs + i;
name = g_string_new("generic-fuzz");
g_string_append_printf(name, "-%s", config->name);
fuzz_add_target(&(FuzzTarget){
//......
});
}
}

fuzz_target_init(register_generic_fuzz_targets);

作者没有设置的device就要自己去设置启动command,获取command的主要函数在generic_fuzz_cmdline中。

接来下看fuzz的前期准备generic_pre_fuzz函数(只截取部分重要代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void generic_pre_fuzz(QTestState *s)
{
//......

if (!getenv("QEMU_FUZZ_OBJECTS")) {
usage();
}

qts_global = s; //设定全局QTestState

dma_regions = g_array_new(false, false, sizeof(address_range));
dma_patterns = g_array_new(false, false, sizeof(pattern));

fuzzable_memoryregions = g_hash_table_new(NULL, NULL);
fuzzable_pci_devices = g_ptr_array_new();

result = g_strsplit(getenv("QEMU_FUZZ_OBJECTS"), " ", -1);
for (int i = 0; result[i] != NULL; i++) {
printf("Matching objects by name %s\n", result[i]);
object_child_foreach_recursive(qdev_get_machine(), // !!!
locate_fuzz_objects,
result[i]);
}
//......
}

先看标感叹号那块代码,经调试,qdev-get-machine()这块得到的machine对象为pc-q35-6.0-machine,如下:

1
2
3
4
5
6
7
8
9
10
pwndbg> p/x *dev
$1 = {
class = 0x614000005240,
free = 0x7ffff6ccfba0,
properties = 0x61d000098f00,
ref = 0x2,
parent = 0x604000016cd0
}
pwndbg> x/1s dev.class.type.name
0x603000009b20: "pc-q35-6.0-machine"

继续看object_child_foreach_recursive这个函数,这个函数比较绕,调用层次也比较多,如果要细说估计都可以成一篇文章了,我就大概理一下思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int do_object_child_foreach(Object *obj,
int (*fn)(Object *child, void *opaque),
void *opaque, bool recurse)
{
GHashTableIter iter;
ObjectProperty *prop;
int ret = 0;

g_hash_table_iter_init(&iter, obj->properties); //properties为一个GHashTable,初始化递归表
while (g_hash_table_iter_next(&iter, NULL, (gpointer *)&prop)) {//逐个遍历递归表
if (object_property_is_child(prop)) {//判断遍历到的ObjectProperty->type是否为child
Object *child = prop->opaque; //取出child object

ret = fn(child, opaque); //执行回调函数
if (ret != 0) { //回调函数返回值不为0才退出循环
break;
}
if (recurse) { //递归执行该函数,也就是递归遍历child
ret = do_object_child_foreach(child, fn, opaque, true);
if (ret != 0) {
break;
}
}
}
}
return ret;
}

为什么Object还有child Object?两者是什么关系?搞明白这个才能清楚这个函数的具体含义。

我举个例子,就拿前面我们得到的对象pc-q35-6.0-machine来举例,这个对象类似于一个根节点,定义在pc-q35.c代码文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DEFINE_Q35_MACHINE(v6_0, "pc-q35-6.0", NULL,
pc_q35_6_0_machine_options);

#define DEFINE_Q35_MACHINE(suffix, name, compatfn, optionfn) \
static void pc_init_##suffix(MachineState *machine) \
{ \
pc_q35_init(machine); \
} \
DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn)

// #define TYPE_MACHINE_SUFFIX "-machine"
#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \
static const TypeInfo pc_machine_type_##suffix = { \
.name = namestr TYPE_MACHINE_SUFFIX, \
.parent = TYPE_PC_MACHINE, \
.class_init = pc_machine_##suffix##_class_init, \
}; \
static void pc_machine_init_##suffix(void) \
{ \
type_register(&pc_machine_type_##suffix); \
} \
type_init(pc_machine_init_##suffix) //跟其他设备的初始化一样

再看看pc_q35_init这个函数:

1
2
3
4
5
6
7
8
static void pc_q35_init(MachineState *machine)
{
q35_host = Q35_HOST_DEVICE(qdev_new(TYPE_Q35_HOST_DEVICE)); //q35-pcihost

object_property_add_child(qdev_get_machine(), "q35", OBJECT(q35_host)); // !!!(1)

//......
}

标记1处的函数从名称可以看出是给objectproperty添加child,那么根据函数定义的参数名可以断定是给qdev_get_machine()所得到的object,也就是pc-q35-6.0-machine新增一个property,命名为q35,子对象为q35-pcihost。也就是在两个对象间新建一个链接关系,类似于链表,并对这个链表命名。简单来说创建的链条如下:

1
2
3
4
5
ObjectProperty *op;

op->name = "q35";
op->type = "child<q35-pcihost>";
op->opaque = OBJECT(q35_host);

当然了,这个“根”对象的child肯定不止这一个。通过对qdev_get_machine()的交叉引用可以发现mc146818rtc.c也是它的child

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void rtc_realizefn(DeviceState *dev, Error **errp)
{
//......
ISADevice *isadev = ISA_DEVICE(dev);
RTCState *s = MC146818_RTC(dev);
object_property_add_tm(OBJECT(s), "date", rtc_get_date); //(1)
//......
}

ISADevice *mc146818_rtc_init(ISABus *bus, int base_year, qemu_irq intercept_irq)
{
ISADevice *isadev;
//......
isadev = isa_new(TYPE_MC146818_RTC);

object_property_add_alias(qdev_get_machine(), "rtc-time", OBJECT(isadev),
"date"); //(2)
return isadev;
}

object_property_add_tm函数创建的propertytype类型为struct tmobject_property_add_alias函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectProperty *
object_property_add_alias(Object *obj, const char *name,
Object *target_obj, const char *target_name)
{
ObjectProperty *op;
ObjectProperty *target_prop;
//......
target_prop = object_property_find_err(target_obj, target_name,
&error_abort);
prop_type = g_strdup(target_prop->type);
op = object_property_add(obj, name, prop_type,
property_get_alias,
property_set_alias,
property_release_alias,
prop);
}

object_property_add_alias所设置的typetarget_objtarget_nametype,也就是ISADevice对象中名为datatype,为struct tm。结合起来看,总的来说创建的链条如下:

1
2
3
4
5
ObjectProperty *op;

op->name = "rtc-time";
op->type = "struct tm";
op->opaque = OBJECT(isadev);

下面就简单说一下链条,链条的type类型有多种,有child<*>link<*>stringboolstruct tmuint8uint16uint32uint64等等,分别对应着父子对象之间的关系类型。定义不同type的链条有不同的函数,如object_property_add_tm()对应struct tmtypeobject_property_add_bool()对应booltype,等等。但最终都会调用同一个函数object_property_try_add()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ObjectProperty *
object_property_try_add(Object *obj, const char *name, const char *type,
ObjectPropertyAccessor *get,
ObjectPropertyAccessor *set,
ObjectPropertyRelease *release,
void *opaque, Error **errp)
{
ObjectProperty *prop;
size_t name_len = strlen(name);

//......

prop = g_malloc0(sizeof(*prop));

prop->name = g_strdup(name); //设置链条name
prop->type = g_strdup(type); //设置链条type

prop->get = get;
prop->set = set;
prop->release = release;
prop->opaque = opaque;

g_hash_table_insert(obj->properties, prop->name, prop); //插入父对象的properties哈希表中
return prop;
}

这下objectchild object就搞明白了,继续看do_object_child_foreach()函数,应该就很明朗了。

这个函数的作用就是,遍历根对象(pc-q35-6.0-machine)的子对象,筛选出type类型是child<*>的子对象,执行回调函数,并且递归遍历子对象继续执行相同的操作,一直循环反复,直到将所有子对象、子对象的子对象…都遍历完后就退出。

根据调试情况可以验证我们的分析:

1
2
3
4
5
6
7
8
9
10
11
// 1
pwndbg> x/1s prop.name
0x6020000440b0: "rtc-time"
pwndbg> x/1s prop.type
0x6020000440d0: "struct tm"

// 2
pwndbg> x/1s prop.name
0x60200002f8d0: "q35"
pwndbg> x/1s prop.type
0x60300005f4a0: "child<q35-pcihost>"

回到generic_fuzz.c主函数中来,分析一下前面提到的回调函数locate_fuzz_objects()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int locate_fuzz_objects(Object *child, void *opaque)
{
char *pattern = opaque; //传入的QEMU_FUZZ_OBJECTS的值
if (g_pattern_match_simple(pattern, object_get_typename(child))) { //匹配该obj name是否为传入字符串
//寻找并保存子对象的memory内存空间
object_child_foreach_recursive(child, locate_fuzz_memory_regions, NULL);

//对比是否属于pci设备,是则保存该对象
if (object_dynamic_cast(OBJECT(child), TYPE_PCI_DEVICE)) {
//为了避免重复保存,先删除再添加
g_ptr_array_remove_fast(fuzzable_pci_devices, PCI_DEVICE(child));
g_ptr_array_add(fuzzable_pci_devices, PCI_DEVICE(child));
}
} else if (object_dynamic_cast(OBJECT(child), TYPE_MEMORY_REGION)) { //匹配是否为memory object继承关系
if (g_pattern_match_simple(pattern, //匹配memory obj与父节点间的type name是否为传入字符串
object_get_canonical_path_component(child))) {
MemoryRegion *mr;
mr = MEMORY_REGION(child); //获取该对象的memory空间
if ((memory_region_is_ram(mr) || //判断是否为内存空间
memory_region_is_ram_device(mr) ||
memory_region_is_rom(mr)) == false) {
g_hash_table_insert(fuzzable_memoryregions, mr, (gpointer)true); //保存memory
}
}
}
return 0;
}

static int locate_fuzz_memory_regions(Object *child, void *opaque)
{
const char *name;
MemoryRegion *mr;
if (object_dynamic_cast(child, TYPE_MEMORY_REGION)) { //判断是否有父子链接关系
mr = MEMORY_REGION(child); //如果有则获取该对象的memory空间
if ((memory_region_is_ram(mr) || //判断memory空间是否为内存空间等等
memory_region_is_ram_device(mr) ||
memory_region_is_rom(mr)) == false) {
name = object_get_canonical_path_component(child);
/*
* We don't want duplicate pointers to the same MemoryRegion, so
* try to remove copies of the pointer, before adding it.
*/
g_hash_table_insert(fuzzable_memoryregions, mr, (gpointer)true); //将memory插入哈希表保存
}
}
return 0;
}

先简要介绍一下两个函数,第一个是object_dynamic_cast(),该函数是用来判断两者object是否有父子关系(继承关系)的。由于这里用来判断是否和TYPE_MEMORY_REGION有继承关系,我查了一下QEMU目前没有继承自TYPE_MEMORY_REGION的情况,因此,这里这个函数的作用就是只判断传入的object是否为TYPE_MEMORY_REGION对象。

第二个是object_get_canonical_path_component(),该函数是获取传入的object与父对象间的链接关系的名字,也就是链条ObjectProperty->name

locate_fuzz_objects函数中,当匹配到我们要fuzz的对象字符串时,又开始递归筛选child子对象,并执行locate_fuzz_memory_regions回调函数。该回调函数的作用是判断传入的child对象是否是TYPE_MEMORY_REGION对象,是就将memory空间保存。后续又判断该对象是否属于PCI继承的设备,属于则保存指针。

再往下看,如果匹配的不是我们要fuzz的对象字符串,而是TYPE_MEMORY_REGION对象,并且该对象与父对象的链条名称和我们输入的字符串相同的话,则保存memory空间。

回到最开始的地方来:

1
2
3
object_child_foreach_recursive(qdev_get_machine(),
locate_fuzz_objects,
result[i]);

短短一行就展开了这么多的知识点,简单来概括一下前面所讲述的内容。

根据传入的QEMU_FUZZ_OBJECTS的值,假设传入了“virtio*”,那么该函数就是从“根”对象machine开始,不断的循环遍历子对象(继承对象),筛选出名称与“virtio*”相匹配的对象,或者是在一对继承关系中继承链条的名称与“virtio*”相匹配的子对象(且该子对象必须为TYPE_MEMORY_REGION),取出这些对象的MEMORY_REGION区域并保存,也相应的保存满足上述条件而且又属于PCI设备的对象。

结合上手调试,理解的会更快一些。拿virtio-vga设备举例,我想要fuzz该设备,传入的QEMU_FUZZ_OBJECT的值为virtio*,那么当QEMU启动后,会匹配所有virtio*相关的对象或者memory region,其中,vga-pci.c文件中有这么一条:

1
2
memory_region_init_io(&subs[0], owner, &pci_vga_ioport_ops, s,
"vga ioports remapped", PCI_VGA_IOPORT_SIZE);

也就是会与memory_region构建一条名为vga ioports remapped的关系链条。当然还有许多其他条相关的链,经调试得出的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
* vga ioports remapped[0] (size 20)
* qemu extended regs[0] (size 8)
* virtio-pci-device[0] (size 1000)
* vga[4] (size 1)
* msix-pba[0] (size 8)
* vga[2] (size 10)
* bochs dispi interface[0] (size 16)
* virtio-pci-notify[0] (size 1000)
* vga[0] (size 2)
* bus master[0] (size 0)
* msix-table[0] (size 30)
* vga-lowmem[0] (size 20000)
* virtio-pci-common[0] (size 800)
* virtio-pci-notify-pio[0] (size 4)
* vbe[0] (size 4)
* bus master container[0] (size 0)
* virtio-vga-msix[0] (size 1000)
* vga[3] (size 2)
* virtio-pci-isr[0] (size 800)
* virtio-pci[0] (size 4000)
* vga[1] (size 1)

总共会保存这些memory region。其中就包括了前面我们所说的vga ioports remapped

继续往下看generic_pre_fuzz()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void generic_pre_fuzz(QTestState *s)
{
//......
printf("This process will try to fuzz the following MemoryRegions:\n");

g_hash_table_iter_init(&iter, fuzzable_memoryregions);
while (g_hash_table_iter_next(&iter, (gpointer)&mr, NULL)) {
printf(" * %s (size %lx)\n",
object_get_canonical_path_component(&(mr->parent_obj)),
(uint64_t)mr->size);
} //循环打印所保存的region,输出就是上面提到的结果

if (!g_hash_table_size(fuzzable_memoryregions)) {
printf("No fuzzable memory regions found...\n");
exit(1);
}

pcibus = qpci_new_pc(s, NULL); //初始化总线数据
g_ptr_array_foreach(fuzzable_pci_devices, pci_enum, pcibus); //以libqos(QTest)的形式初始化各个保存的pci设备
qpci_free_pc(pcibus);

counter_shm_init(); //初始化fork下的共享内存
}

这一块就是收尾工作,将PCI总线和保存的PCI设备初始化,总的来说就是做一些fuzz前的初始化准备工作。重点讲一下回调函数pci_enum()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void pci_enum(gpointer pcidev, gpointer bus)
{
PCIDevice *dev = pcidev;
QPCIDevice *qdev;
int i;

qdev = qpci_device_find(bus, dev->devfn); //根据device number、function number创建QTest
//模式的pci device
g_assert(qdev != NULL);
for (i = 0; i < 6; i++) {
if (dev->io_regions[i].size) {
qpci_iomap(qdev, i, NULL);
}
}
qpci_device_enable(qdev);
g_free(qdev);
}

dev->io_regions[]这个比较重要,是PCI Configuration space中的六个bar空间,但是看定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define PCI_NUM_REGIONS 7

struct PCIDevice {
//......
PCIIORegion io_regions[PCI_NUM_REGIONS];
}

typedef struct PCIIORegion {
pcibus_t addr; /* current PCI mapping address. -1 means not mapped */
#define PCI_BAR_UNMAPPED (~(pcibus_t)0)
pcibus_t size;
uint8_t type;
MemoryRegion *memory;
MemoryRegion *address_space;
} PCIIORegion;

实际上region7个,为什么上面只遍历了6个?因为最后一个region实际上是ROM空间,前六个是RAM空间,在这里我们用不到ROM,因此只遍历六个。具体来看下面的代码,这个代码用于给device添加option rom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void pci_add_option_rom(PCIDevice *pdev, bool is_default_rom,
Error **errp)
{
//......
pci_register_bar(pdev, PCI_ROM_SLOT, 0, &pdev->rom); //PCIDevice中有个名为rom的region
}

void pci_register_bar(PCIDevice *pci_dev, int region_num,
uint8_t type, MemoryRegion *memory)
{
//......
if (region_num == PCI_ROM_SLOT) { //PCI_ROM_SLOT = 6
/* ROM enable bit is writable */
wmask |= PCI_ROM_ADDRESS_ENABLE;
}
}

基本可以看出来是用于ROM的。同样的,这几个region的初始化来源也是pci_register_bar()函数,不过该函数只是注册了一下,并没有分配到实际的地址,地址指的是换成上述结构体来说的话就是PCIIORegion->addr。更新地址的函数在pci_update_mapping()。其中io_regions的内容都是从MemoryRegion结构体中迁移过来的。具体怎么注册bar空间的后续会提到一部分。这一块推荐看这篇文章

继续看qpci_iomap()函数,这个函数信息量比较大,我拓展开来看了好久才明白这个函数是做什么的。简单概括一下就是以QTest的形式重新分配PCI设备的bar地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
QPCIBar qpci_iomap(QPCIDevice *dev, int barno, uint64_t *sizeptr)
{
QPCIBus *bus = dev->bus;
static const int bar_reg_map[] = {
PCI_BASE_ADDRESS_0, PCI_BASE_ADDRESS_1, PCI_BASE_ADDRESS_2,
PCI_BASE_ADDRESS_3, PCI_BASE_ADDRESS_4, PCI_BASE_ADDRESS_5,
};
QPCIBar bar;
int bar_reg;
uint32_t addr, size;
uint32_t io_type;
uint64_t loc;

g_assert(barno >= 0 && barno <= 5);
bar_reg = bar_reg_map[barno];

qpci_config_writel(dev, bar_reg, 0xFFFFFFFF); //写bar空间的地址(1)
addr = qpci_config_readl(dev, bar_reg); //读bar空间的地址(1)

io_type = addr & PCI_BASE_ADDRESS_SPACE; //判断bar空间是memory map还是io map
if (io_type == PCI_BASE_ADDRESS_SPACE_IO) {
addr &= PCI_BASE_ADDRESS_IO_MASK;
} else {
addr &= PCI_BASE_ADDRESS_MEM_MASK;
}

g_assert(addr); /* Must have *some* size bits */

size = 1U << ctz32(addr); //获取bar空间的size(2)
if (sizeptr) {
*sizeptr = size;
}

//......
}

这里有两个比较疑惑的点,在标记1处,先写bar地址为0xFFFFFFFF,后读bar地址,不过当我调试的时候,得到的addr并不是0xFFFFFFFF。第二点在标记2处,ctz32()是取末尾零的个数,为什么这样就能得到bar空间的size

这两个点可以结合起来一起看。前面我们提到注册io_regions的函数为pci_register_bar()。这里我们仍然以virtio-vga设备为栗子。在virtio-vga.c初始化设备中存在注册函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static void virtio_vga_base_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
//......
pci_register_bar(&vpci_dev->pci_dev, 0,
PCI_BASE_ADDRESS_MEM_PREFETCH, &vga->vram); //注册0号位的bar空间(总共六个)
}

void pci_register_bar(PCIDevice *pci_dev, int region_num,
uint8_t type, MemoryRegion *memory)
{
PCIIORegion *r;
uint32_t addr; /* offset in pci config space */
uint64_t wmask;
pcibus_t size = memory_region_size(memory);
uint8_t hdr_type;

assert(region_num >= 0);
assert(region_num < PCI_NUM_REGIONS);
assert(is_power_of_2(size));

/* A PCI bridge device (with Type 1 header) may only have at most 2 BARs */
hdr_type = //取配置空间的Header Type部分
pci_dev->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;
assert(hdr_type != PCI_HEADER_TYPE_BRIDGE || region_num < 2);

r = &pci_dev->io_regions[region_num]; //取相应注册的bar空间,这里是0号
r->addr = PCI_BAR_UNMAPPED; //分配地址,这里是未映射地址0xFFFFFFFFFFFFFFFF
r->size = size; //分配size,取自memory reigon
r->type = type; //分配type
r->memory = memory; //分配memory
r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
? pci_get_bus(pci_dev)->address_space_io
: pci_get_bus(pci_dev)->address_space_mem;

wmask = ~(size - 1); //(3)
if (region_num == PCI_ROM_SLOT) {
/* ROM enable bit is writable */
wmask |= PCI_ROM_ADDRESS_ENABLE;
}

addr = pci_bar(pci_dev, region_num); //取相应的bar空间的地址
pci_set_long(pci_dev->config + addr, type); //写bar的type类型

if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
pci_set_quad(pci_dev->wmask + addr, wmask);
pci_set_quad(pci_dev->cmask + addr, ~0ULL);
} else {
pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff); //写wmask的值
pci_set_long(pci_dev->cmask + addr, 0xffffffff);
}
}

在调试过程当中,上述vga设备注册的MemoryRegion->size0x800000,再看标记3处,wmask的结果为0xff800000PCIDevice->wmask的定义是用于实现R/W字节,应该是用于标记size的作用。最终配置空间的内存情况是这样的:

1
2
3
4
5
6
7
8
pwndbg> x/10xg 0x62100002bd00
0x62100002bd00: 0x0010000010501af4 0x0000000003000001
0x62100002bd10: 0x0000000000000008 0x000000000000000c
0x62100002bd20: 0x0000000000000000 0x11001af400000000
0x62100002bd30: 0x0000009800000000 0x0000010000000000
0x62100002bd40: 0x0000000201100009 0x0000080000001000

//0x62100002bd10处的0x08值,就是所写入的bar type值。可见,bar处并没有配置地址。

回到qpci_iomap()函数,当调用标记1处的函数qpci_config_writel()时,最终会调用hw/pci/pci.c:pci_default_write_config()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//传入参数:addr = 0x10 , val_in = 0xffffffff

void pci_default_write_config(PCIDevice *d, uint32_t addr, uint32_t val_in, int l)
{
uint32_t val = val_in;

for (i = 0; i < l; val >>= 8, ++i) {
uint8_t wmask = d->wmask[addr + i]; //取wmask值
uint8_t w1cmask = d->w1cmask[addr + i];
assert(!(wmask & w1cmask));
d->config[addr + i] = (d->config[addr + i] & ~wmask) | (val & wmask); //(4)
d->config[addr + i] &= ~(val & w1cmask); /* W1C: Write 1 to Clear */
}
//......
}

关键读写的操作在标记4处。简单来说就是设置bar空间的地址。根据前面我们知道wmask的结果是0xff800000,那么最终设置的bar地址就是0xff800000,内存布局如下:

1
2
3
4
5
6
7
8
pwndbg> x/10xg 0x62100002bd00
0x62100002bd00: 0x0010000010501af4 0x0000000003000001
0x62100002bd10: 0x00000000ff800008 0x000000000000000c
0x62100002bd20: 0x0000000000000000 0x11001af400000000
0x62100002bd30: 0x0000009800000000 0x0000010000000000
0x62100002bd40: 0x0000000201100009 0x0000080000001000

//设置过后的0x62100002bd10地址处的值。

这也就是为什么前面我们在qpci_iomap()函数标记1先写后读bar地址为什么会出现不是0xffffffff的情况。最终得到的地址是0xff800008

第一个疑惑点解决了,再来看第二个疑惑点,为什么1U << ctz32(addr);就能获得size。当去掉addr地址的type执行到标记2处的时候,addr的值是0xff800000ctz32(addr)得到的就是231<<23就是size,即0x800000,和最开始注册时的size是一样的。

根据定义,memory space barI/O space bar的地址分别是16byte4byte对齐的:

字节对齐

这就是为什么能按上述来获取size的原因,以及为什么能够利用wmask来做辅助。还不清楚可以自己动手算一下….

qpci_iomap()前半部分算是分析完了,再来看剩余的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QPCIBar qpci_iomap(QPCIDevice *dev, int barno, uint64_t *sizeptr)
{
//......
} else {
loc = QEMU_ALIGN_UP(bus->mmio_alloc_ptr, size); //取基数mmio alloc指针

bus->mmio_alloc_ptr = loc + size; //将基数到基数+size这部分空间留给该io_region

qpci_config_writel(dev, bar_reg, loc); //重新设置bar地址
}

bar.addr = loc;
return bar;
}

就举例mmio的情况,pio其实一样。最开始的bus->mmio_allco_ptr0xE0000000

1
2
3
4
5
6
7
void qpci_init_pc(QPCIBusPC *qpci, QTestState *qts, QGuestAllocator *alloc)
{
//......
qpci->bus.pio_alloc_ptr = 0xc000; //pio基址
qpci->bus.mmio_alloc_ptr = 0xE0000000; //mmio基址
qpci->bus.mmio_limit = 0x100000000ULL; //mmio限度
}

后半部分其实就是从地址0xE0000000开始往后的空间依次分配给PCIbar空间。qpci_iomap()函数到这里分析就结束了。

再次回到pci_enum()函数中来,最后剩下的qpci_device_enable()函数就是写PCI设备的配置空间COMMAND内容处,使得PCI设备能够正式启用。

至此,正式fuzz前的准备工作函数generic_fuzz()都已经分析完毕,所有细节部分我们都已经了解过了。剩下的就只有正式fuzz函数以及变异策略函数了。

正式fuzz函数我就不细说,主要就是取libfuzzer的随机输入数据做拆分并设置几个opcode做选择。举个栗子,现在有这么一串随机输入数据:

00 01 02 FF 03 04 05 06 FF 01 FF

我设置了几个opcode函数:

1
2
3
4
5
OP_IN,
OP_OUT,
OP_READ,
OP_WRITE
...

0xFF做分割符,分别取出来作为datadata_len,那么这一串数据就分别对应一下函数:

1
2
3
* 00 01 02    -> op00 (0102)   -> in (0102, 2)
* 03 04 05 06 -> op03 (040506) -> write (040506, 3)
* 01 -> op01 (-,0) -> out (-,0)

这就是主fuzz函数的核心思想。

这里我们再看一下获取mmiopio地址的关键函数get_io_address()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct get_io_cb_info {
int index;
int found;
address_range result;
};

typedef struct {
ram_addr_t addr;
ram_addr_t size; /* The number of bytes until the end of the I/O region */
} address_range;

static bool get_io_address(address_range *result, AddressSpace *as,
uint8_t index,
uint32_t offset) { //传入的为全局的address_space_memory变量,
//代表虚拟地址空间
FlatView *view;
view = as->current_map; //获取当前的FlatView
g_assert(view);
struct get_io_cb_info cb_info = {};

cb_info.index = index;

do {
flatview_for_each_range(view, get_io_address_cb , &cb_info);
} while (cb_info.index != index && !cb_info.found);

*result = cb_info.result;
if (result->size) {
offset = offset % result->size;
result->addr += offset;
result->size -= offset;
}
return cb_info.found;
}

void flatview_for_each_range(FlatView *fv, flatview_cb cb , void *opaque)
{
FlatRange *fr;

assert(fv);
assert(cb);

FOR_EACH_FLAT_RANGE(fr, fv) { //从根开始循环遍历FlatRange
if (cb(fr->addr.start, fr->addr.size, fr->mr, opaque))
break;
}
}

static int get_io_address_cb(Int128 start, Int128 size,
const MemoryRegion *mr, void *opaque) {
struct get_io_cb_info *info = opaque;
if (g_hash_table_lookup(fuzzable_memoryregions, mr)) { //查询是否包含在前面所存储的region中
if (info->index == 0) {
info->result.addr = (ram_addr_t)start;
info->result.size = (ram_addr_t)size;
info->found = 1;
return 1;
}
info->index--; //遍历直到idx为0,也就是选择先前保存的所有region中第idx个region
}
return 0;
}

阅读上面的代码需要理解QEMU的内存管理机制,不明白的推荐看这两篇,QEMU对虚拟机的内存管理QEMU内存模型

简单说就是随机选取前面保存起来的MemoryRegion中的一块,进行后续的读写操作。

但是,这里有读者可能就有疑问了,如果要随机选的话,为什么不直接在存储起来的空间中直接挑呢?而是大费周章的去全局遍历,对比,然后再挑选?

因为我们最终目的是要得到MemoryRegion中的地址、以及size。虽然说看了MemoryRegion的结构体后能够发现其本身就有一个addr属性,但是,这个addr并不是真正意义上的内存地址,还是一个相对偏移,即类似于offset。在QEMU内存管理中,FlatRange中有指向所属MemoryRegion的指针,其中也保存着addrsize,这里的addr才是MemoryRegion真正的地址,具体结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct AddrRange {
Int128 start; //起始
Int128 size; //大小
};

/* Range of memory in the global map. Addresses are absolute. */
struct FlatRange {
MemoryRegion *mr; //指向所属的MR
hwaddr offset_in_region; //在MR中的offset
AddrRange addr; //本FR代表的区间
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;
};

这下就明白为什么要“大费周章”的去遍历所有Region得到addr了吧。具体的细节看完上面两篇文章后就能弄清楚整一个内存管理的原理了。

最后就是变异策略的函数了,这一块作者写的比较简单,自行阅读代码即可。不过我注意到作者还写了fuzz_dma_read_cb()函数,但是并没有应用过,觉得没有用吗?这一块读者感兴趣的话可以自己阅读以下,看看具体作用到底是什么。

源码分析到这里就完全结束了,感谢阅读。有错误欢迎斧正。

0x03 Summary

读到这里,我想聪明的读者应该已经发现,其实generic_fuzz是一个比较dumbfuzzer,优点就在能够通用fuzz。而且因为受限于QTest,只有部分设备做了QTest化的处理,所以能够测试的目标有限,我猜测这也是为什么作者只写了针对virtio的几个设备写了特定的fuzz,因为官方在QTest中只写了virtio的一部分。如果想要更高效率的fuzz的话,那还是得需要自己做优化的,我这里仅提供几个思路。

  • generic_fuzz中做结构化fuzz,也就是争对某个device做相应的结构体输入化,这样可以充分利用该fuzzfork的优势。缺点是目标单一,不能多设备fuzz。这里也可以删除他本身的几个opcode函数,例如读写config的可以删除,对于我们来说基本没什么用,可以只保留读写mmio/pio区域的函数,这样可以提高fuzz效率。

  • 自己编写QTest化的设备代码,并后续针对这个继续写相应的fuzz。也就是承接上面官方只写了一部分的情况。这是个体力活,不过我个人估计产出会比较明显。

  • generic_fuzz上做优化(不是单一化),例如提高覆盖率等操作,我自认为它本身还有比较大的改进空间,具体怎么改,我还没有可行的思路,知道的读者麻烦交流一下:)

0x04 Reference

  1. https://www.cnblogs.com/ccxikka/p/9477530.html
  2. https://richardweiyang-2.gitbook.io/kernel-exploring/00-kvm/01-memory_virtualization/01_1-qemu_memory_model
  3. https://qemu.readthedocs.io/en/latest/devel/qgraph.html#qgraph
  4. https://blog.csdn.net/weixin_43780260/article/details/104410063
支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫